package com.free.bsf.eureka.client.version;

import com.netflix.discovery.DiscoveryClient;
import com.free.bsf.core.base.BsfException;
import com.free.bsf.core.util.*;
import com.free.bsf.eureka.client.EurekaProperties;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import lombok.val;
import lombok.var;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;

public class EurekaVersionExtend {
    final static String  VERSION_KEY = "bsf_eureka_version";
    /**
     * 初始化版本号到metadata
     */
    public static void initVersion(Map<String,String> metaDataMap){
        if(!PropertyUtils.getPropertyCache(EurekaProperties.BsfEurekaVersionClientEnabled,false))
            return;
        var mvcControllerMap = ContextUtils.getBean(RequestMappingHandlerMapping.class,false);
        val versionMap = new HashMap<String,Double>();
        if(mvcControllerMap!=null){
            var clss = new ArrayList<Class>();
           for(val method : mvcControllerMap.getHandlerMethods().values()){
               if(!clss.contains(method.getBeanType())){
                   clss.add(method.getBeanType());
               }
           }
           for(var cls:clss){
               versionMap.putAll(getVersion(cls));
           }
            metaDataMap.put(VERSION_KEY, JsonUtils.serialize(versionMap));
        }
    }
    private static volatile boolean isLoadHookInitVersion=false;
    public static void hookInitVersion(){
        if(!PropertyUtils.getPropertyCache(EurekaProperties.BsfEurekaVersionClientEnabled,false))
            return;
        try {
            String code = "{"+
                    "		com.free.bsf.eureka.client.version.EurekaVersionExtend.initVersion($1.getMetadataMap());\n"+
                    "       return $0.setupJmxPortOld($$);\n" +
                    "}";
            //EurekaClientAutoConfiguration.setupJmxPort
            ClassPool classPool = ClassPoolUtils.getInstance();
            CtClass ctClass = classPool.get("org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration");
            if (!isLoadHookInitVersion) {
                isLoadHookInitVersion = true;
                CtMethod ctMethod = ctClass.getDeclaredMethod("setupJmxPort");
                CtMethod mold = CtNewMethod.copy(ctMethod, "setupJmxPortOld", ctClass, null);
                ctClass.addMethod(mold);
                ctMethod.setBody(code);

                if (ctClass.isFrozen()) {
                    ctClass.defrost();
                }
                ctClass.toClass();
                LogUtils.info(EurekaVersionExtend.class, EurekaProperties.Project, "注入EurekaClientAutoConfiguration ok");
            }
        } catch (Exception exp) {
            LogUtils.error(EurekaVersionExtend.class,  EurekaProperties.Project, "注入EurekaClientAutoConfiguration 异常", exp);
        }
    }
    private static List<String> filterMethods=new ArrayList<>(Arrays.stream(Object.class.getMethods()).map(Method::getName).collect(Collectors.toList()));
    private static HashMap<String,Double> getVersion(Class cls){
        HashMap<String,Double> rs = new HashMap<>();
        var  cVersion = (EurekaVersion)ReflectionUtils.getFirstAnnotation(cls,EurekaVersion.class);
        for(var method:cls.getMethods()) {
            if(filterMethods.contains(method.getName()))
                continue;
            var mVersion = (EurekaVersion)ReflectionUtils.getFirstAnnotation(cls,method,EurekaVersion.class);
            String clsName=cls.getName();
            if(cVersion!=null&& !StringUtils.isEmpty(cVersion.name())){
                clsName = cVersion.name();
            }
            String mName = method.getName();
            if(mVersion!=null&& !StringUtils.isEmpty(mVersion.name())){
                mName = mVersion.name();
            }
            Double version = 0D;
            if(mVersion!=null&&mVersion.version()>0){
                version = mVersion.version();
            }else if(cVersion!=null&&cVersion.version()>0){
                version = cVersion.version();
            }
            if (version >0) {
                rs.put(clsName+"-"+mName, version);
            }
        }
        return rs;
    }

    /**
     * 版本校验
     */
    private static HashMap<String,Double> LocalVersionMap = new HashMap<>();
    public static void loadLocalVersion(String clsName){
        var cls = ReflectionUtils.tryClassForName(clsName);
        if(cls!=null) {
            LocalVersionMap.putAll(getVersion(cls));
        }
    }
    private static volatile boolean isLoadHookLoadLocalVersion=false;
    public static void hookLoadLocalVersion(){
        try {
            if(!PropertyUtils.getPropertyCache(EurekaProperties.BsfEurekaVersionFeginEnabled,false))
                return;
            String code = "{"+
                    "		com.free.bsf.eureka.client.version.EurekaVersionExtend.loadLocalVersion($2.getClassName());\n"+
                    "       return $0.registerFeignClientOld($$);\n" +
                    "}";
            //FeignClientsRegistrar.registerFeignClient
            ClassPool classPool = ClassPoolUtils.getInstance();
            CtClass ctClass = classPool.get("org.springframework.cloud.openfeign.FeignClientsRegistrar");
            if (!isLoadHookLoadLocalVersion) {
                isLoadHookLoadLocalVersion = true;
                CtMethod ctMethod = ctClass.getDeclaredMethod("registerFeignClient");
                CtMethod mold = CtNewMethod.copy(ctMethod, "registerFeignClientOld", ctClass, null);
                ctClass.addMethod(mold);
                ctMethod.setBody(code);

                if (ctClass.isFrozen()) {
                    ctClass.defrost();
                }
                ctClass.toClass();
                LogUtils.info(EurekaVersionExtend.class, EurekaProperties.Project, "注入FeignClientsRegistrar ok");
            }
        } catch (Exception exp) {
            LogUtils.error(EurekaVersionExtend.class,  EurekaProperties.Project, "注入FeignClientsRegistrar 异常", exp);
        }
    }

    private static volatile boolean isLoadHookCheckVersion=false;
    public static void hookCheckVersion(){
        try {
            if(!PropertyUtils.getPropertyCache(EurekaProperties.BsfEurekaVersionFeginEnabled,false))
                return;
            String code = "{"+
                    "		com.free.bsf.eureka.client.version.EurekaVersionExtend.checkVersion($1);\n"+
                    "       return $0.setDiscoveryClientOld($$);\n" +
                    "}";
            //DiscoveryManager.setDiscoveryClient
            ClassPool classPool = ClassPoolUtils.getInstance();
            CtClass ctClass = classPool.get("com.netflix.discovery.DiscoveryManager");
            if (!isLoadHookCheckVersion) {
                isLoadHookCheckVersion = true;
                CtMethod ctMethod = ctClass.getDeclaredMethod("setDiscoveryClient");
                CtMethod mold = CtNewMethod.copy(ctMethod, "setDiscoveryClientOld", ctClass, null);
                ctClass.addMethod(mold);
                ctMethod.setBody(code);

                if (ctClass.isFrozen()) {
                    ctClass.defrost();
                }
                ctClass.toClass();
                LogUtils.info(EurekaVersionExtend.class, EurekaProperties.Project, "注入DiscoveryManager ok");
            }
        } catch (Exception exp) {
            LogUtils.error(EurekaVersionExtend.class,  EurekaProperties.Project, "注入DiscoveryManager 异常", exp);
        }
    }
    public static void checkVersion(DiscoveryClient discoveryClient){
            if(!PropertyUtils.getPropertyCache(EurekaProperties.BsfEurekaVersionFeginEnabled,false))
                return;
            var remoteVersionMap = new HashMap<String,Double>();
            var disCoverClient = (discoveryClient!=null?discoveryClient:ContextUtils.getBean(DiscoveryClient.class, false));
            if(disCoverClient!=null) {
                var apps = disCoverClient.getApplications();
                for (var app : apps.getRegisteredApplications()) {
                    if (app.getInstances().size() > 0) {
                        for(var first:app.getInstances()) {  
                            var versionMapStr = first.getMetadata().get(VERSION_KEY);
                            if (!StringUtils.isEmpty(versionMapStr)) {
                                try {
                                    var versinMap = (HashMap<String, Double>) JsonUtils.deserialize(versionMapStr, remoteVersionMap.getClass());
                                    for(Entry<String, Double> entry:versinMap.entrySet()) {
                                        if(remoteVersionMap.containsKey(entry.getKey())) {
                                            if(entry.getValue()>remoteVersionMap.get(entry.getKey())) {
                                                remoteVersionMap.put(entry.getKey(),entry.getValue());
                                            }
                                        }else {
                                            remoteVersionMap.put(entry.getKey(),entry.getValue());
                                        }
                                        
                                    }
                                } catch (Exception e) {
                                    LogUtils.warn(EurekaVersionExtend.class, EurekaProperties.Project, "反序列化eureka服务版本出错:" + first.getAppName(), e);
                                }
                            }
                        }
                    }
                }
                for (var kv : LocalVersionMap.entrySet()) {
                    if (remoteVersionMap.containsKey(kv.getKey())) {
                        if (kv.getValue() > remoteVersionMap.get(kv.getKey())) {
                            throw new BsfException(kv.getKey() + "版本校验出错,本地版本:" + kv.getValue() + ">远程版本:" + remoteVersionMap.get(kv.getKey()));
                        }
                    }
                }
            }
        }
}
